package controller

import (
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
	"tmlake.com/api-gateway/istio-agent/pkg/model"
	"tmlake.com/api-gateway/istio-agent/pkg/pretty"
	njt_log "tmlake.com/api-gateway/istio-agent/proxy"
	"tmlake.com/api-gateway/istio-agent/proxy/file"
	"tmlake.com/api-gateway/istio-agent/proxy/njetcontroller/authtls"
	"tmlake.com/api-gateway/istio-agent/proxy/njetcontroller/configs"
	njt_config "tmlake.com/api-gateway/istio-agent/proxy/njetcontroller/controller/config"
	njt_template "tmlake.com/api-gateway/istio-agent/proxy/njetcontroller/controller/template"
	"tmlake.com/api-gateway/istio-agent/proxy/njetcontroller/jwt"
	"tmlake.com/api-gateway/istio-agent/proxy/njetcontroller/proxyssl"
	"tmlake.com/api-gateway/istio-agent/proxy/njetcontroller/resolver"
	"tmlake.com/api-gateway/istio-agent/xds/httpfilter"
	"tmlake.com/api-gateway/istio-agent/xds/xdsclient/xdsresource"
	"tmlake.com/api-gateway/pkg/security"
)

const (
	defUpstreamName = "upstream-default-backend"
	defServerName   = "_"
	rootLocation    = "/"
	njet502Server   = "unix:/var/lib/njet/njet-502-server.sock"
	njet404Server   = "unix:/var/lib/njet/njet-404-server.sock"
)

// Configuration contains all the settings required by an api-gateway controller
type Configuration struct {
	ListenPorts             *njt_config.ListenPorts
	PostShutdownGracePeriod int
	ShutdownGracePeriod     int
	Node                    *model.Node
}

// DefaultEndpoint returns the default endpoint to be use as default server that returns 404.
func (n NJETController) DefaultEndpoint() configs.Endpoint {
	return configs.Endpoint{
		Address: njet502Server,
		Port:    "",
	}
}

// getDefaultUpstream returns the upstream associated with the default backend.
func (n *NJETController) getDefaultUpstream() *configs.Backend {
	upstream := &configs.Backend{
		Name:      defUpstreamName,
		Endpoints: []configs.Endpoint{},
	}

	upstream.Endpoints = append(upstream.Endpoints, n.DefaultEndpoint())
	return upstream

}

func (n NJETController) Default404Endpoint() configs.Endpoint {
	return configs.Endpoint{
		Address: njet404Server,
		Port:    "",
	}
}

func (n *NJETController) get404Upstream() *configs.Backend {
	upstream := &configs.Backend{
		Name:      configs.UpstreamName404,
		Endpoints: []configs.Endpoint{},
	}

	upstream.Endpoints = append(upstream.Endpoints, n.Default404Endpoint())
	return upstream

}

func (n *NJETController) createUpstreamWithSSL(backend *configs.Backend, clusters map[string]xdsresource.ClusterUpdate) {
	if backend == nil {
		return
	}

	for clusterName, cu := range clusters {
		if backend.Name == clusterName {
			if cu.SecurityCfg == nil {
				backend.SSLMode = configs.Client_DISABLE
				break
			}
			backend.ProxySSL = proxyssl.Config{
				VerifyDepth: 10,
			}

			if len(cu.SecurityCfg.CommonTlsContext.ValidationContext.SubjectAltNameMatchers) != 0 {
				backend.ProxySSL.ProxySSLName = cu.SecurityCfg.CommonTlsContext.ValidationContext.SubjectAltNameMatchers[0].GetMatcher()
			}

			if cu.SecurityCfg.CommonTlsContext.ValidationContextSdsSecretConfig != nil {
				backend.ProxySSL.AuthSSLCert.CACertificate = cu.SecurityCfg.CommonTlsContext.ValidationContextSdsSecretConfig.Name
				if cu.SecurityCfg.CommonTlsContext.ValidationContextSdsSecretConfig.Name == security.RootCertReqResourceName {
					backend.SSLMode = configs.Client_ISTIO_MUTUAL
				} else if strings.HasPrefix(cu.SecurityCfg.CommonTlsContext.ValidationContextSdsSecretConfig.Name, configs.Client_MUTUAL_Root_Prefix) {
					backend.SSLMode = configs.Client_MUTUAL
				}
			}

			if cu.SecurityCfg.CommonTlsContext.TlsCertificateSdsSecretConfigs != nil && len(cu.SecurityCfg.CommonTlsContext.TlsCertificateSdsSecretConfigs) != 0 {
				backend.SSLCert.SSLCertificate = cu.SecurityCfg.CommonTlsContext.TlsCertificateSdsSecretConfigs[0].Name
				if cu.SecurityCfg.CommonTlsContext.TlsCertificateSdsSecretConfigs[0].Name == security.WorkloadKeyCertResourceName {
					backend.SSLMode = configs.Client_ISTIO_MUTUAL
				} else if strings.HasPrefix(cu.SecurityCfg.CommonTlsContext.TlsCertificateSdsSecretConfigs[0].Name, configs.Client_MUTUAL_Workload_Prefix) {
					backend.SSLMode = configs.Client_MUTUAL
				}
			}
			break
		}
	}
}

// createUpstreams creates the NJET upstreams (Endpoints) for each Service
func (n *NJETController) createUpstreams(data map[string]xdsresource.EndpointsUpdate, upstreams map[string]*configs.Backend, clusters map[string]xdsresource.ClusterUpdate) map[string]*configs.Backend {
	for upstreamName, endpoints := range data {
		upstreams[upstreamName] = newUpstream(upstreamName)
		n.createUpstreamWithSSL(upstreams[upstreamName], clusters)
		for _, locality := range endpoints.Localities {
			for _, e := range locality.Endpoints {
				str := strings.Split(e.Address, ":")
				var address, port string
				if len(str) == 2 {
					address = str[0]
					port = str[1]
				} else {
					address = str[0]
					port = ""
				}
				upstreams[upstreamName].Endpoints = append(upstreams[upstreamName].Endpoints, configs.Endpoint{
					Address: address,
					Port:    port,
				})
			}
		}
	}

	njt_log.NjetLogger.Info("Creating upstream starting...")
	for upstreamName, backend := range upstreams {
		njt_log.NjetLogger.Infof("%s=%+v", upstreamName, backend.Endpoints)
	}
	njt_log.NjetLogger.Infof("Creating upstream end..., total=%d", len(upstreams))

	return upstreams
}

func (n *NJETController) createServers(data map[string]xdsresource.RouteConfigUpdate, upstreams map[string]*configs.Backend) map[string]*configs.VirtualServerConfig {
	vsCfgs := make(map[string]*configs.VirtualServerConfig, len(data))

	for routeName, routeConfig := range data {
		for _, virtualHost := range routeConfig.VirtualHosts {
			if len(virtualHost.Domains) == 0 || (len(virtualHost.Domains) == 1 && virtualHost.Domains[0] == "*") {
				continue
			}
			routes := configs.MergeRouteForSamePath(routeName, virtualHost)
			njt_log.NjetLogger.Errorf("virtualHostName:%q MergeRoutes:%s", virtualHost.Name, pretty.ToJSON(routes))
			vsCfg := configs.GenerateVirtualServerConfig(routeName, virtualHost, routes)
			vsCfgs[virtualHost.Name] = vsCfg
		}
	}

	njt_log.NjetLogger.Info("Traversal server starting...")
	for virtualHostName, vsConfig := range vsCfgs {
		njt_log.NjetLogger.Info("virtualHostName:%s:routeNameOfXDS=%q, domains=%+v, loc=%+v", virtualHostName, vsConfig.Server.RouteName, vsConfig.Server.Hostnames, vsConfig.Server.Locations)
		njt_log.NjetLogger.Info("virtualHostName:%q Server:%s", virtualHostName, pretty.ToJSON(vsConfig.Server))
		njt_log.NjetLogger.Info("virtualHostName:%q MergeRoutes:%s", virtualHostName, pretty.ToJSON(vsConfig.MergeRoutes))
		njt_log.NjetLogger.Info("virtualHostName:%q Maps:%s", virtualHostName, pretty.ToJSON(vsConfig.Maps))
	}
	njt_log.NjetLogger.Infof("Traversal server end..., total=%d", len(vsCfgs))

	return vsCfgs
}

func (n *NJETController) getBackendServers(routeConfigs map[string]xdsresource.RouteConfigUpdate, endpoints map[string]xdsresource.EndpointsUpdate, clusters map[string]xdsresource.ClusterUpdate) ([]*configs.Backend, []*configs.VirtualServerConfig) {
	dus := make(map[string]*configs.Backend)
	du := n.getDefaultUpstream()
	du404 := n.get404Upstream()
	dus[defUpstreamName] = du
	dus[configs.UpstreamName404] = du404
	upstreams := n.createUpstreams(endpoints, dus, clusters)
	servers := n.createServers(routeConfigs, upstreams)

	n.checkUpstreamServer(servers, upstreams)

	aUpstreams := make([]*configs.Backend, 0, len(upstreams))

	for _, upstream := range upstreams {
		aUpstreams = append(aUpstreams, upstream)
	}

	aServers := make([]*configs.VirtualServerConfig, 0, len(servers))
	for _, value := range servers {
		sort.SliceStable(value.Server.Locations, func(i, j int) bool {
			return value.Server.Locations[i].Path > value.Server.Locations[j].Path
		})

		sort.SliceStable(value.Server.Locations, func(i, j int) bool {
			return len(value.Server.Locations[i].Path) > len(value.Server.Locations[j].Path)
		})
		aServers = append(aServers, value)
	}

	sort.SliceStable(aUpstreams, func(a, b int) bool {
		return aUpstreams[a].Name < aUpstreams[b].Name
	})

	sort.SliceStable(aServers, func(i, j int) bool {
		return aServers[i].Name < aServers[j].Name
	})

	return aUpstreams, aServers
}

func (n *NJETController) checkUpstreamServer(servers map[string]*configs.VirtualServerConfig, upstreams map[string]*configs.Backend) {
	if servers == nil && upstreams == nil {
		return
	}

	originalServerTotal := len(servers)
	originalUpstreamTotal := len(upstreams)
	var deleteUpstreams []string
	//handling proxy test errors
	//proxy: [emerg] host not found in upstream "outbound|15010||istiod.istio-system.svc.cluster.local" in /tmp/proxy/proxy-cfg4156149296:330
	//330 line:proxy_pass http://outbound|15010||istiod.istio-system.svc.cluster.local;
	for _, vsConf := range servers {
		for _, loc := range vsConf.Server.Locations {
			backend := &loc.UpstreamName
			find := false
			for upstreamName, upstream := range upstreams {
				if *backend != upstream.Name {
					continue
				}

				find = true
				if len(upstream.Endpoints) == 0 {
					delete(upstreams, upstreamName)
					deleteUpstreams = append(deleteUpstreams, upstreamName)
					njt_log.NjetLogger.Warningf("server name: %q, Location: \"%+v\" although matching backend, but backend endpoint is empty, so delete upstream: %q, "+
						"and set up the default backend", vsConf.Name, loc, upstreamName)
					*backend = defUpstreamName
				}
				break
			}

			if !find {
				njt_log.NjetLogger.Warningf("server name: %q, Location: \"%+v\" no matching backend, and set up the default backend", vsConf.Name, loc)
				*backend = defUpstreamName
			}
		}
	}

	//handling proxy test errors
	//proxy: [emerg] no servers are inside upstream in /tmp/proxy/proxy-cfg401563673:72
	for upstreamName, upstream := range upstreams {
		if len(upstream.Endpoints) == 0 {
			upstream.Endpoints = []configs.Endpoint{{Address: njet502Server}}
			njt_log.NjetLogger.Warningf("upstream not in use, upstream: %q, endpoint is empty", upstreamName)
			continue
		}
	}

	finalServerTotal := len(servers)
	finalUpstreamTotal := len(upstreams)

	njt_log.NjetLogger.Infof("originalServerTotal:%d, originalUpstreamTotal:%d, finalServerTotal:%d, finalUpstreamTotal:%d, deleteUpstream len:%d, deleteUpstream list:%q",
		originalServerTotal, originalUpstreamTotal, finalServerTotal, finalUpstreamTotal, originalUpstreamTotal-finalUpstreamTotal, deleteUpstreams)
}

func (n *NJETController) getConfiguration(routeConfigs map[string]xdsresource.RouteConfigUpdate, endpoints map[string]xdsresource.EndpointsUpdate,
	listenerConfig *xdsresource.InboundListenerConfig, clusters map[string]xdsresource.ClusterUpdate) *configs.Configuration {
	upstreams, servers := n.getBackendServers(routeConfigs, endpoints, clusters)

	return &configs.Configuration{
		Backends:      upstreams,
		Servers:       servers,
		TCPL4Services: n.getL4Services(listenerConfig),
	}
}

func (n *NJETController) getL4Services(listenerConfig *xdsresource.InboundListenerConfig) map[string]*configs.L4Service {
	l4Services := make(map[string]*configs.L4Service)
	if listenerConfig == nil {
		return l4Services
	}
	port, _ := strconv.Atoi(listenerConfig.Port)
	l4Services[listenerConfig.ListenerName] = &configs.L4Service{
		Port:                                  port,
		DestinationPortPeerAuthenticationMode: make(map[string]configs.PeerAuthenticationMode),
		CertificateAuth:                       make(map[string]authtls.Config),
		SSLCert:                               make(map[string]*configs.SSLCert),
	}

	//Collect all protocols for each matched port, no duplicate removal
	filterChainMatchInfo := make(map[string]*[]string)
	portSecurityCfg := make(map[string]*xdsresource.SecurityConfig)
	portJWTCfg := make(map[string]httpfilter.FilterConfig)
	for _, filterChain := range listenerConfig.FilterChains.FilterChains {
		isNew := false
		if filterChain.FilterChainMatch != nil {
			if filterChain.FilterChainMatch.DestinationPort == 0 {
				continue
			}
			var transportProtocol []string
			if cur, ok := filterChainMatchInfo[strconv.Itoa(int(filterChain.FilterChainMatch.DestinationPort))]; ok {
				//It may happen that TransportProtocol is tls, but SecurityCfg is nil, this is the configuration delivered by istio
				if filterChain.FilterChainMatch.TransportProtocol == configs.Tls && filterChain.SecurityCfg != nil {
					portSecurityCfg[strconv.Itoa(int(filterChain.FilterChainMatch.DestinationPort))] = filterChain.SecurityCfg
					*cur = append(*cur, filterChain.FilterChainMatch.TransportProtocol)
				} else if filterChain.FilterChainMatch.TransportProtocol == configs.RawBuffer {
					*cur = append(*cur, filterChain.FilterChainMatch.TransportProtocol)
				}
			} else {
				transportProtocol = append(transportProtocol, filterChain.FilterChainMatch.TransportProtocol)
				//The matching port first appearance and
				//has a new protocol
				isNew = true

				if filterChain.FilterChainMatch.TransportProtocol == configs.Tls && filterChain.SecurityCfg != nil {
					portSecurityCfg[strconv.Itoa(int(filterChain.FilterChainMatch.DestinationPort))] = filterChain.SecurityCfg
				} else if filterChain.FilterChainMatch.TransportProtocol == configs.Tls && filterChain.SecurityCfg == nil {
					//The invalid TLS
					isNew = false
				}
			}

			if isNew {
				filterChainMatchInfo[strconv.Itoa(int(filterChain.FilterChainMatch.DestinationPort))] = &transportProtocol
			}

			//deal with JWT
			for _, hf := range filterChain.HTTPFilters {
				if hf.Name == httpfilter.HttpFilterJwtAuthentication {
					if hf.Config == nil {
						break
					}
					if _, ok := portJWTCfg[strconv.Itoa(int(filterChain.FilterChainMatch.DestinationPort))]; !ok {
						portJWTCfg[strconv.Itoa(int(filterChain.FilterChainMatch.DestinationPort))] = hf.Config
					}

					break
				}
			}
		}
	}

	l4Services[listenerConfig.ListenerName].JwtAuthenticationOfPort = jwt.CreateJWTConfg(portJWTCfg)
	njt_log.NjetLogger.Debugf("ListenerName:%q, JwtAuthenticationOfPort:%+v\n", listenerConfig.ListenerName, l4Services[listenerConfig.ListenerName].JwtAuthenticationOfPort)

	destinationPortPeerAuthenticationMode := make(map[string]configs.PeerAuthenticationMode)
	for destinationPort, peerAuthentications := range filterChainMatchInfo {
		isTLS := false
		if len(*peerAuthentications) >= 2 {
			isRaw, isT := false, false
			for _, peer := range *peerAuthentications {
				if peer == configs.Tls {
					isT = true
				} else {
					isRaw = true
				}
			}

			if isRaw && isT {
				destinationPortPeerAuthenticationMode[destinationPort] = configs.PERMISSIVE
				isTLS = true
			} else if isRaw {
				destinationPortPeerAuthenticationMode[destinationPort] = configs.DISABLE
			} else if isT {
				destinationPortPeerAuthenticationMode[destinationPort] = configs.STRICT
				isTLS = true
			} else {
				destinationPortPeerAuthenticationMode[destinationPort] = configs.DISABLE
			}

		} else if len(*peerAuthentications) == 1 {
			switch (*peerAuthentications)[0] {
			case configs.Tls:
				destinationPortPeerAuthenticationMode[destinationPort] = configs.STRICT
				isTLS = true
			case configs.RawBuffer:
				destinationPortPeerAuthenticationMode[destinationPort] = configs.DISABLE
			default:
				destinationPortPeerAuthenticationMode[destinationPort] = configs.DISABLE
			}
		}

		if isTLS {
			l4Services[listenerConfig.ListenerName].CertificateAuth[destinationPort] = authtls.Config{
				VerifyClient:    portSecurityCfg[destinationPort].RequireClientCert,
				ValidationDepth: 10,
				AuthSSLCert: resolver.AuthSSLCert{
					CACertificate: portSecurityCfg[destinationPort].CommonTlsContext.ValidationContextSdsSecretConfig.Name,
				},
			}
			l4Services[listenerConfig.ListenerName].SSLCert[destinationPort] = &configs.SSLCert{
				SSLCertificate: portSecurityCfg[destinationPort].CommonTlsContext.TlsCertificateSdsSecretConfigs[0].Name,
			}
		}

	}

	l4Services[listenerConfig.ListenerName].DestinationPortPeerAuthenticationMode = destinationPortPeerAuthenticationMode

	return l4Services
}

func (n *NJETController) GetBackendConfiguration() njt_config.Configuration {
	return n.backendConfig
}

// OnUpdate is called by the synchronization loop whenever configuration
// changes were detected.
// Returns nil in case the backend was successfully reloaded.
func (n *NJETController) OnUpdate(xdsCfg configs.Configuration) error {
	cfg := n.GetBackendConfiguration()
	cfg.LuaSharedDicts = njt_template.DefaultLuaSharedDicts
	logLevel := os.Getenv("API_GATEWAY_LOG_SEVERITY_LEVEL")
	vLevel := os.Getenv("API_GATEWAY_LOG_VERBOSITY_LEVEL")
	//[debug | info | notice | warn | error | crit | alert | emerg]
	switch logLevel {
	case "debug", "DEBUG":
		cfg.ErrorLogLevel = "debug"
	case "info", "INFO":
		cfg.ErrorLogLevel = "info"
		if vl, err := strconv.Atoi(vLevel); err == nil {
			if vl >= 2 {
				cfg.ErrorLogLevel = "debug"
			}
		}
	case "notice", "NOTICE":
		cfg.ErrorLogLevel = "notice"
	case "warn", "warning", "WARN", "WARNING":
		cfg.ErrorLogLevel = "warn"
	case "", "error", "ERROR":
		cfg.ErrorLogLevel = "error"
	case "crit", "CRIT":
		cfg.ErrorLogLevel = "crit"
	case "alert", "ALERT":
		cfg.ErrorLogLevel = "alert"
	case "emerg", "EMERG":
		cfg.ErrorLogLevel = "emerg"
	default:
		cfg.ErrorLogLevel = "error"
	}

	content, err := n.generateTemplate(cfg, xdsCfg)
	if err != nil {
		njt_log.NjetLogger.Errorf("generateTemplate err")
		return err
	}

	err = n.testTemplate(content)
	if err != nil {
		return err
	}

	err = os.WriteFile(cfgPath, content, file.ReadWriteByUser)
	if err != nil {
		return err
	}

	o, err := n.command.ExecCommand("-s", "reload").CombinedOutput()
	if err != nil {
		njt_log.NjetLogger.Errorf("proxy reload err. %v\n%v", err, string(o))
		return fmt.Errorf("%v\n%v", err, string(o))
	}

	return nil
}

// syncXDS collects all the pieces required to assemble the NJET
// configuration file and passes the resulting data structures to the backend
// (OnUpdate) when a reload is deemed necessary.
func (n *NJETController) syncXDS(obj interface{}) error {
	routeConfigs := n.r.Client.MapRouteConfigs()
	endpoints := n.r.Client.MapEndpoints()
	clusters := n.r.Client.MapClusters()

	var pcfg *configs.Configuration
	if listenerConfig, ok := obj.(*xdsresource.InboundListenerConfig); ok {
		pcfg = n.getConfiguration(routeConfigs, endpoints, listenerConfig, clusters)
		n.virtualInboundListenerConfig = listenerConfig
	} else {
		pcfg = n.getConfiguration(routeConfigs, endpoints, n.virtualInboundListenerConfig, clusters)
	}

	if n.runningConfig.Equal(pcfg) {
		njt_log.NjetLogger.Infof("No configuration change detected, skipping backend reload")
		return nil
	}

	njt_log.NjetLogger.Infof("Configuration changes detected, backend reload required")
	err := n.OnUpdate(*pcfg)
	if err != nil {
		njt_log.NjetLogger.Errorf("Unexpected failure reloading the backend:\n%v", err)
		return err
	}

	njt_log.NjetLogger.Info("Backend successfully reloaded")

	n.runningConfig = pcfg

	return nil
}
